package view;

import java.awt.Graphics2D;

import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.Timer;

import model.*;

/**
 * PacGameCanvas provides the main game window for a game of
 * tb-pacman. 
 * 
 * This canvas will handle the display of all
 * non-interactive elements of a game of tb-pacman
 * (i.e. the game maze, score of life displays... etc.).
 * The main purpose of this class (above that of PacCanvas)
 * is to provide smooth animation, and to listen on a 
 * GameModel object.
 * 
 * In order to provide a smooth game experience, this
 * canvas is double buffered. The smooth animation of game
 * sprites is a bit of a kludge (using swing timers), but
 * thread interference was an issue I was not able to tackle
 * given the time frame.
 * 
 * @author Alexander Craig
 * @author Robert Gillespie
 */
@SuppressWarnings("serial")
public class PacGameCanvas extends PacCanvas implements GameListener, ActionListener{
	
	/** how long a single tile movement should take (in ms) */
	private static final long MOVE_TIME = 250;
	
	/** game messages used to display information to the player */
	private static final String MSG_LEVEL_START = "Ready!";
	private static final String MSG_LEVEL_PASSED = "Good Job! Hit Space for Next Level";
	private static final String MSG_PLAYER_DEATH = "You Died";
	private static final String MSG_GAME_OVER = "Game Over. You Lose :'(";
	private static final String MSG_WIN = "You Win! Congratulations!";
	
	/** the length of time in ms that a frame lasts */
	private static final int FRAME_LENGTH = 25;
	
	/** the GameModel to listen on */
	private GameModel model;
	/** timer used for smooth drawing of game movements */
	private Timer frameTimer;
	/** the system time (in ms) when the current move should end */
	private long moveEnd;
	/** the system time (in ms) of the last update */
	private long lastFrameMs;
	
	
	/**
	 * Makes a new instance of PacCanvas, listening on the specified
	 * GameModel. Each PacCanvas can only listen on one GameModel
	 * at a time, and will ignore all other events.
	 * @param model	the model to listen to.
	 */
	public PacGameCanvas(GameModel model) {
		this.model = model;
		model.addGameListener(this);
		
		// Setup the timer used for smooth movement animation
		frameTimer = new Timer(FRAME_LENGTH, this);
		
		// Setup all the strokes used for drawing game elements
		moveEnd = 0;
		lastFrameMs = System.currentTimeMillis();
		loadMaze(model.getMaze());
	}
	
	/**
	 * @return	true if a move is currently being drawn
	 */
	public boolean isMoving() {
		if (moveEnd == 0) {
			return false;
		} else {
			return moveEnd - System.currentTimeMillis() <= 0;
		}
	}
	
	@Override
	/**
	 * Set the displayed overlay to the game over message.
	 */
	public void gameOver(GameEvent event) {
		if (getOverlay() != MSG_WIN) {
			setOverlay(MSG_GAME_OVER);
		}
	}
	
	/**
	 * Translates the passed Graphics object for character movement
	 * based on the movement end time, the last frame time, and the direction of
	 * character movement.
	 * @param dir	the movement direction of the game entity in question
	 * @param g2D	the graphics object to translate
	 * @param currentTime	The current system time in ms. This needs to be a parameter
	 * 						as this function could be called several times in a single frame,
	 * 						and should use the same time for every call in a frame.
	 */
	private void translate(Direction dir, Graphics2D g2D) {
		if (moveEnd == 0 || g2D == null || dir == null) {
			return;
		}
	
		double translateFactor = (moveEnd - lastFrameMs) / (double)MOVE_TIME;
		
		switch(dir) {
		case NORTH:
			g2D.translate(0, TILE_SPREAD * translateFactor);
			break;
		case SOUTH:
			g2D.translate(0, -TILE_SPREAD * translateFactor);
			break;
		case EAST:
			g2D.translate(-TILE_SPREAD * translateFactor, 0);
			break;
		case WEST:
			g2D.translate(TILE_SPREAD * translateFactor, 0);
			break;
		}
	}
	
	/**
	 * Loads rendering settings into the passed graphics object based on the
	 * current selected graphics present.
	 * @see PacCanvas.setSettings()
	 * @param g2D	the graphics object to load settings for
	 */
	public void loadSettings(Graphics2D g2D) {
		super.loadSettings(g2D);
		switch(getSettings()) {
		case PacCanvas.SETTINGS_HIGH:
		case PacCanvas.SETTINGS_MID:
			frameTimer.setDelay(25);
			break;
		case PacCanvas.SETTINGS_LOW:
			frameTimer.setDelay(50);
			break;
		}
	}
	
	
	public void drawBackBuffer(Graphics2D buffedG2D)
	{
		// Get the time of the start of the current frame
		lastFrameMs = System.currentTimeMillis();
		if(moveEnd != 0 && lastFrameMs >= moveEnd) {
			moveEnd = 0;
			frameTimer.stop();
		}
		
		// Save the current drawing matrix
		AffineTransform saveAT = buffedG2D.getTransform();
		drawMazeLower(buffedG2D); // Draw the lower maze layer
		drawMazeDots(buffedG2D); // White dots in the centre of unvisited tiles
		
	    translate(model.getPlayer().getLastMove(), buffedG2D); // Translate the drawing matrix for smooth movement
	    drawPacman(model.getPlayer(), buffedG2D); // Pacman
		buffedG2D.setTransform(saveAT); // Revert translation changes
		
		for(int i = 0; i < model.getNumGhosts(); i++) {
			translate(model.getGhost(i).getLastMove(), buffedG2D); // Translate the drawing matrix for smooth movement
			drawGhost(model.getGhost(i), buffedG2D); // Ghosts
			buffedG2D.setTransform(saveAT); // Revert translation changes
		}
		
		drawMazeUpper(buffedG2D); // Draw the upper maze layer

	    drawStatusUI(model.getPlayer(), buffedG2D); // UI (status indicators)
	    drawOverlay(buffedG2D); // Draw the overlay string
	}

	@Override
	/**
	 * Set the displayed overlay to the level successful message.
	 */
	public void levelComplete(GameEvent event) {
		if(getOverlay() != MSG_GAME_OVER && getOverlay() != MSG_PLAYER_DEATH) {
			if(event.getSource().lastLevel()) {
				setOverlay(MSG_WIN);
			} else {
				setOverlay(MSG_LEVEL_PASSED);
			}
		}
	}

	@Override
	/**
	 * Resets the drawing canvas.
	 */
	public void playerDeath(GameEvent event) {
		setOverlay(MSG_PLAYER_DEATH);
		repaint();
	}

	@Override
	/**
	 * Sets up the timer and moveEnd variable for smooth sprite movement.
	 */
	public void playerMove(GameEvent event) {
		frameTimer.restart();
		moveEnd = System.currentTimeMillis() + MOVE_TIME;
		setOverlay(null);
		this.repaint();	
	}
	
	@Override
	/**
	 * Resets the drawing canvas.
	 */
	public void newGame(GameEvent event) {
		moveEnd = 0;
		frameTimer.stop();
		setOverlay(MSG_LEVEL_START);
		repaint();
		
	}
	
	@Override
	/**
	 * Sets the overlay to the new level message.
	 */
	public void levelReset(GameEvent event) {
		loadMaze(event.getSource().getMaze());
		setOverlay(MSG_LEVEL_START);
	}
}
